9.3 XGBoost#
In der bisherigen Vorlesung haben wir vor allem Pandas und Scikit-Learn benutzt. Zwar bietet Scikit-Learn Boosting-Verfahren an, in vielen Wettbewerben hat sich jedoch eine andere Bibliothek durchgesetzt, die eine optimierte Variante des Stochastic Gradient Boosting anbietet: XGBoost.
Warnung
Falls bei Ihnen XGBoost nicht installiert sein sollte, folgen Sie bitte den Anweisungen auf der Internesetseite https://xgboost.readthedocs.io und installieren Sie XGBoost jetzt nach.
Lernziele#
Lernziele
Sie können XGBoost für Regressions- und Klassifikationsaufgaben einsetzen.
Sie wissen, wie Sie mit Analysen der Maßzahlen Fehler und Logloss für Trainings- und Testdaten beurteilen können, ob Überanpassung (Overfitting) vorliegt.
Sie kennen die Methode Frühes Stoppen zur Reduzierung der Überanpassung (Overfitting).
Sie wissen, dass XGBoost nicht manuell feinjustiert werden sollte, sondern mit Gittersuche oder weiteren Bibliotheken (z.B. Optuna).
XGBoost benutzt Scikit-Learn API#
XGBoost steht für eXtreme Gradient Boosting und ist aus Performancegründen in der Programmiersprache C++ implementiert. Für Python-Programmier wurde ein Python-Modul mit dem Ziel geschaffen, die gleichen Schnittstellen wie Scikit-Learn anzubieten, so dass kaum Einarbeitungszeit in eine neue Bibliothek erforderlich ist. Vor allem benötigen Data Scientists auch keine C++-Programmierkenntnisse, sondern können weiterhin mit Python arbeiten.
Wir bleiben bei unserem Beispiel mit der Verkaufsaktion im Autohaus aus dem vorherigen Kapitel.
import pandas as pd
from sklearn.datasets import make_moons
# Erzeugung künstlicher Daten
X_array, y_array = make_moons(n_samples=120, random_state=0, noise=0.3)
daten = pd.DataFrame({
'Kilometerstand [km]': 10000 * (X_array[:,0] + 2),
'Preis [EUR]': 5000 * (X_array[:,1] + 2),
'verkauft': y_array,
})
XGBoost kann Pandas DataFrames nicht verarbeiten, sondern benötigt die reinen
Zahlenwerte in Form von Matrizen. Das ist in der Tat kein Problem, denn die
Datenstruktur DataFrame stellt die reinen Matrizen über die Methode .values
direkt zur Verfügung.
# Adaption der Daten
X = daten[['Kilometerstand [km]', 'Preis [EUR]']].values
y = daten['verkauft'].values
Als nächstes importieren wir XGBoost. Es ist üblich, das ganze Modul zu
importieren und mit xgb abzukürzen. Danach initialisieren wir das
Klassifikationsmodell XGBClassifier und trainieren es auf den Daten.
import xgboost as xgb
modell = xgb.XGBClassifier()
modell.fit(X,y)
XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=None, device=None, early_stopping_rounds=None,
enable_categorical=False, eval_metric=None, feature_types=None,
gamma=None, grow_policy=None, importance_type=None,
interaction_constraints=None, learning_rate=None, max_bin=None,
max_cat_threshold=None, max_cat_to_onehot=None,
max_delta_step=None, max_depth=None, max_leaves=None,
min_child_weight=None, missing=nan, monotone_constraints=None,
multi_strategy=None, n_estimators=None, n_jobs=None,
num_parallel_tree=None, random_state=None, ...)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=None, device=None, early_stopping_rounds=None,
enable_categorical=False, eval_metric=None, feature_types=None,
gamma=None, grow_policy=None, importance_type=None,
interaction_constraints=None, learning_rate=None, max_bin=None,
max_cat_threshold=None, max_cat_to_onehot=None,
max_delta_step=None, max_depth=None, max_leaves=None,
min_child_weight=None, missing=nan, monotone_constraints=None,
multi_strategy=None, n_estimators=None, n_jobs=None,
num_parallel_tree=None, random_state=None, ...)Als nächstes visualisieren wir die Prognose des trainierten XGBoost-Klassifikators.
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.colors import ListedColormap
from sklearn.inspection import DecisionBoundaryDisplay
my_colormap = ListedColormap(['#EF553B33', '#636EFA33'])
fig = DecisionBoundaryDisplay.from_estimator(modell, X, cmap=my_colormap)
fig.ax_.scatter(X[:,0], X[:,1], c=y, cmap=my_colormap)
fig.ax_.set_xlabel('Kilometerstand [km]');
fig.ax_.set_ylabel('Preis [EUR]');
fig.ax_.set_title('XGBoost: Entscheidungsgrenzen');
Die Entscheidungsgrenzen sehr plausibel aus.
XGBoost neigt stark zur Überanpassung (Overfitting)#
XGBoost ist bekannt für Überanpassung (Overfitting) an die Trainingsdaten. Um
das an unserem Beispiel mit der Verkaufsaktion im Autohaus zu zeigen, fügen wir
noch neue, unbekannte Testdaten hinzu. Dazu verdoppeln wir die Anzahl der Autos
(n_samples=2000).
# Erzeugung künstlicher Daten
X_array, y_array = make_moons(n_samples=2000, random_state=0, noise=0.3)
daten = pd.DataFrame({
'Kilometerstand [km]': 10000 * (X_array[:,0] + 2),
'Preis [EUR]': 5000 * (X_array[:,1] + 2),
'verkauft': y_array,
})
X = daten[['Kilometerstand [km]', 'Preis [EUR]']].values
y = daten['verkauft'].values
Anschließend teilen wir die 2000 Autos in zwei Gruppen: Trainings- und Testdaten.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,y, train_size=0.5, random_state=0)
Diesmal legen wir explizit fest, aus wievielen Modellen das Boosting-Verfahren
bestehen soll. Dazu setzen wir n_estimators=200. Oft wird auch von der Anzahl
der »Boosting-Runden« gesprochen. Das Training auf den Trainingsdaten liefert
sehr gute Ergebnisse:
import xgboost as xgb
modell = xgb.XGBClassifier(n_estimators=200)
modell.fit(X_train, y_train)
score_train = modell.score(X_train, y_train)
print(f'Score bezogen auf Trainingsdaten: {score_train:.2f}')
score_test = modell.score(X_test, y_test)
print(f'Score bezogen auf Testdaten: {score_test:.2f}')
Score bezogen auf Trainingsdaten: 1.00
Score bezogen auf Testdaten: 0.87
Die Trainingsdaten werden perfekt prognostiziert. Auch bei den Testdaten erhalten wir ein gutes Ergebnis, das aber im Vergleich zu dem sehr guten Score bei den Trainingsdaten abfällt. Es fällt schwer, zu entscheiden, ob eine Überanpassung (Overfitting) vorliegt. XGBoost ist ein iteratives Verfahren. Zunächst wird Modell Nr. 1 trainiert, darauf aufbauend Modell Nr. 2 usw. Wir wiederholen jetzt das Training des XGBoost-Klassifikators, aber lassen durch ein weiteres Argument mitprotokollieren, was in den einzelnen Iterationen passiert.
Zuerst legen wir fest, welche internen Bewertungskennzahlen (= Metrik, Maßzahl) mitprotokolliert werden sollen. Wir wählen als erste Maßzahl den Fehler, also die relative Anzahl der falsch klassifizierten Autos. Die zweite Maßzahl berechnet, wie weit die Wahrscheinlichkeit für »verkauft« oder »nicht verkauft« vom tatsächlichen Ergebnis weg ist. Mathmatisch etwas präziser betrachten wir die Kreuzentropie, bekannt als »Losslog«.
Technisch setzen wir dies um, indem wir bei der Initialisierung des
XGBoost-Modells das optionale Argument eval_metric=['error', 'logloss']
setzen.
modell = xgb.XGBClassifier(n_estimators=200, eval_metric=['error', 'logloss'])
Allerdings ist damit noch nicht festgelegt, auf welchen Daten die Fehler-Maßzahl
und die Logloss-Maßzahl berechnet werden. Zunächst sollen beide Maßzahlen für
die Trainingsdaten berechnet werden, dann für die Testdaten. Das erreichen wir
mit dem optionalen Argument eval_set=, dem wir folgendermaßen die Trainings-
und Testdaten mitgeben.
modell.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_test, y_test)], verbose=False)
XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=None, device=None, early_stopping_rounds=None,
enable_categorical=False, eval_metric=['error', 'logloss'],
feature_types=None, gamma=None, grow_policy=None,
importance_type=None, interaction_constraints=None,
learning_rate=None, max_bin=None, max_cat_threshold=None,
max_cat_to_onehot=None, max_delta_step=None, max_depth=None,
max_leaves=None, min_child_weight=None, missing=nan,
monotone_constraints=None, multi_strategy=None, n_estimators=200,
n_jobs=None, num_parallel_tree=None, random_state=None, ...)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=None, device=None, early_stopping_rounds=None,
enable_categorical=False, eval_metric=['error', 'logloss'],
feature_types=None, gamma=None, grow_policy=None,
importance_type=None, interaction_constraints=None,
learning_rate=None, max_bin=None, max_cat_threshold=None,
max_cat_to_onehot=None, max_delta_step=None, max_depth=None,
max_leaves=None, min_child_weight=None, missing=nan,
monotone_constraints=None, multi_strategy=None, n_estimators=200,
n_jobs=None, num_parallel_tree=None, random_state=None, ...)Wir setzen noch verbose=False, damit nicht für jedes Modell bzw. jede
Iteration die vier Maßzahlen auf dem Bildschirm ausgegeben werden. Nach dem
Training können wir die vier Maßzahlen mit der Methode .evals_result() aus dem
trainierten Modell extrahieren. Um die Maßzahlen zu visualisieren, packen wir
sie in einen Pandas-DataFrame.
masszahlen = modell.evals_result()
fehler = pd.DataFrame({
'Fehler Trainingsdaten': masszahlen['validation_0']['error'],
'Fehler Testdaten': masszahlen['validation_1']['error']
})
losslog = pd.DataFrame({
'Losslog Trainingsdaten': masszahlen['validation_0']['logloss'],
'Losslog Testdaten': masszahlen['validation_1']['logloss']
})
Wir visualisieren Fehler und Losslog getrennt voneinander.
import plotly.express as px
fig = px.scatter(fehler,
title='Fehler in jeder Iteration (Boosting-Runde)',
labels={'value': 'Fehler', 'index': 'Iteration', 'variable': 'Legende'})
fig.show()
Der Fehler bei den Trainingsdaten wird von Boosting-Runde zu Boosting-Runde kleiner, aber der Fehler der Testdaten wächst. Zunächst wird der Fehler der Testdaten kleiner, erreicht in Minimum in der 6. Iteration, um dann wieder zu steigen. Dieses Verhalten ist typisch für Überanpassung (Overfitting). Etwas deutlicher wird dieses Phänomen, wenn wir uns die (transoformierte) Differenz der Wahrscheinlichkeiten ansehen, die Losslog-Maßzahl.
import plotly.express as px
fig = px.scatter(losslog,
title='Losslog in jeder Iteration (Boosting-Runde)',
labels={'value': 'Losslog', 'index': 'Iteration', 'variable': 'Legende'})
fig.show()
Am kleinsten ist die Losslog-Maßzahl für die Iteration 9, danach steigt die Losslog-Maßzahl wieder an. Am besten wäre es nach dieser Analyse gewesen, nach der 6. oder 9. Iteration aufzuhören, da dann die Überanpassung (Overfitting) an die Trainingsdaten einsetzt.
Bekämpfen von Überanpassung (Overfitting)#
Es gibt einige Hyperparamter von XGBoost, die helfen, Überanpassung
(Overfitting) zu reduzieren. Eine Möglichkeit ist es, früher zu stoppen und
nicht die voreingestellte Anzahl an Modellen bzw. Iterationen / Boosting-Runden
zu durchlaufen. Das wird durch das optionale Argument early_stopping_rounds=
im Konstruktor ermöglicht. Die Zahl, die diesem Parameter übergeben wird, gibt
die Anzahl der Boosting-Runden vor, nach denen gestoppt wird, falls sich kaum
etwas an der Maßzahl geändert hat.
modell = xgb.XGBClassifier(n_estimators=200, early_stopping_rounds=10, eval_metric=['error', 'logloss'])
modell.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_test, y_test)], verbose=False)
XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=None, device=None, early_stopping_rounds=10,
enable_categorical=False, eval_metric=['error', 'logloss'],
feature_types=None, gamma=None, grow_policy=None,
importance_type=None, interaction_constraints=None,
learning_rate=None, max_bin=None, max_cat_threshold=None,
max_cat_to_onehot=None, max_delta_step=None, max_depth=None,
max_leaves=None, min_child_weight=None, missing=nan,
monotone_constraints=None, multi_strategy=None, n_estimators=200,
n_jobs=None, num_parallel_tree=None, random_state=None, ...)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
XGBClassifier(base_score=None, booster=None, callbacks=None,
colsample_bylevel=None, colsample_bynode=None,
colsample_bytree=None, device=None, early_stopping_rounds=10,
enable_categorical=False, eval_metric=['error', 'logloss'],
feature_types=None, gamma=None, grow_policy=None,
importance_type=None, interaction_constraints=None,
learning_rate=None, max_bin=None, max_cat_threshold=None,
max_cat_to_onehot=None, max_delta_step=None, max_depth=None,
max_leaves=None, min_child_weight=None, missing=nan,
monotone_constraints=None, multi_strategy=None, n_estimators=200,
n_jobs=None, num_parallel_tree=None, random_state=None, ...)Visualisiert sieht die Losslog-Statistik für das obige Beispiel so aus:
masszahlen = modell.evals_result()
fehler = pd.DataFrame({
'Fehler Trainingsdaten': masszahlen['validation_0']['error'],
'Fehler Testdaten': masszahlen['validation_1']['error']
})
losslog = pd.DataFrame({
'Losslog Trainingsdaten': masszahlen['validation_0']['logloss'],
'Losslog Testdaten': masszahlen['validation_1']['logloss']
})
fig = px.scatter(fehler,
title='Frühes Stoppen: Fehler',
labels={'value': 'Fehler', 'index': 'Iteration', 'variable': 'Legende'})
fig.show()
fig = px.scatter(losslog,
title='Frühes Stoppen: Losslog',
labels={'value': 'Losslog', 'index': 'Iteration', 'variable': 'Legende'})
fig.show()
Eine weitere Möglichkeit, Überanpassung (Overfitting) zu reduzieren, besteht
darin, die Tiefe der Entscheidungsbäume zu begrenzen. Wir benutzen
Entscheidungsbaum-Stümpfe, die eine Tiefe von Eins haben. Das erreichen wir mit
dem optionalen Argument max_depth=1.
modell = xgb.XGBClassifier(max_depth=1, n_estimators=200, eval_metric=['error', 'logloss'])
modell.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_test, y_test)], verbose=False)
masszahlen = modell.evals_result()
fehler = pd.DataFrame({
'Fehler Trainingsdaten': masszahlen['validation_0']['error'],
'Fehler Testdaten': masszahlen['validation_1']['error']
})
losslog = pd.DataFrame({
'Losslog Trainingsdaten': masszahlen['validation_0']['logloss'],
'Losslog Testdaten': masszahlen['validation_1']['logloss']
})
fig = px.scatter(fehler,
title='Begrenzte Entscheidungsbäume: Fehler',
labels={'value': 'Fehler', 'index': 'Iteration', 'variable': 'Legende'})
fig.show()
fig = px.scatter(losslog,
title='Begrenzte Entscheidungsbäume: Losslog',
labels={'value': 'Losslog', 'index': 'Iteration', 'variable': 'Legende'})
fig.show()
Es gibt noch einige weitere Hyperparameter, die für “das” beste Modell feinjustiert werden können. Händisch gelingt es kaum, alle Hyperparameter optimal einzustellen, so dass hier eine Gittersuche oder gar eine Bibliothek wie Optuna eingesetzt werden sollte.
Zusammenfassung und Ausblick#
Mit XGBoost haben Sie ein ML-Modell für das überwachte Lernen kennengelernt, das in den vergangen Jahren sehr viele Wettbewerbe gewonnen hat. Die Mächtigkeit der Algorithmen führt aber häufig zur Überanpassung (Overfitting), so dass die sorgsame Feinjustierung der Hyperparameter besonders wichtig ist.